M6 design amendment 2: forward rank-to-level + re-ranking laws (§2.7.6, closes Sol round-2 blocker)#178
Conversation
Close Sol's round-2 blocker (~/PolicyEngine/sol-worktrees/m6-forward-REPORT.md): §2.7 specified the forward conditional-RANK draw but not the leakage-safe map from a drawn rank to a positive earnings LEVEL for 2016/2018 (the certified de-index run_gate1_candidate7.py:648-654 uses (age_bin, period) marginals that do not exist post-2014), nor its inverse. §2.7.6 pins both with adjudication-grade option analysis, plus the four secondary byte-determinism gaps, so the engine lane implements with zero design choices: - rank-to-level: PIN (b2) a NAWI-normalized calendar-invariant <=2014 age_bin CellMarginal (certified fit_cell_marginals verbatim, key (age_bin,period)-> age_bin), de-indexed then re-indexed by a <=2014-projected wage index; reject (a) donor-level bootstrap (departs from the certified marginal-quantile de-index, an R2 violation, no clean inverse) and (b1) carrying the 2014 period marginal (§2.7.1-prohibited, biases earn_p10 low). - wage index I_proj: OLS ln(NAWI)~year over the trailing decade [2005,2014], extrapolated to 2016/2018; realized post-2014 NAWI on the SCORED path is PROHIBITED -- concretely the frame's realized nawi column (steps.py aging) is fenced off the gated de-index and admissible only for non-scored seam/taxable_max and the report-only alignment path (decision 9, M2 calibration_disclosure; gated path = un-aligned projection). - re-ranking: PIN the fit-population marginal CDF (rhat, the EXACT inverse of the rank-to-level map); reject within-frame percentile (references the synthetic projected cohort, an R2 violation, breaks the round-trip). - secondary gaps: memory-after-2018 rolling generated-lag columns (gen_earn_w2/w4); the seed_from_generator -> SeedSequence([seed,code]) substream bridge; the concrete frame schema; and age-bin clipping to the fitted [25,59]. States the lock dependency: the gate_m6 lock flip's design_commit must finalize to this amendment's merge. Docs-only; no gates.yaml edit, no floor, no test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Adversarial verification — design amendment 2 (§2.7.6,
|
…; NAWI lag
Verifier UNRESOLVED/MAJOR (comment 4960445225):
- MAJOR: the gated older earnings cohort spans 45-64 (EARN_COHORTS
build_m6_holdout_floors.py:120; no panel age cap), but §2.7.6.5 clipped the
law to the gate-1-transferred [25,59], so 60-64 would de-index through the
55-59 marginal and kill earn_zero_rate.older. Per adjudication (option ii),
EXTEND the fit to [25,64]: the forward participation-gate, donor pools,
memory ramp, and NAWI-normalized marginal all fit [25,64] pooled <=2014
(which contains 60-64 + the retirement-transition signal); age_bin grid ->
8 bins {25-29..60-64}; clip <25->25-29, >64->60-64; <25 & 65+ non-scored.
State the principle: the law must cover the surface it is scored on;
narrowing the surface to fit model support is candidate-informed surface
design, prohibited. Re-verified every gated earnings cohort span in [25,64].
- MINOR 1: qualify the round-trip -- exact on the strictly-monotone interior;
corner clamps ([ymin,ymax]/[0.001,0.999]) and flat-yval ties are
deterministic-but-inexact (candidate5b:250-259, corner_bottom/top); the 2018
re-rank is deterministic so chain integrity holds (bounded corner rank
perturbation).
- MINOR 2: disclose the NAWI publication lag (NAWI_2013/2014 published post-2014
but a final/unrevised series, so leakage-safe -- date, not information, is
post-T*); drop the loose "same as certified indexing" analogy.
- MINOR 3: "keeps the certified MECHANISM" (re-fits a new age_bin normalized
object through the same CellMarginal machinery), not "object".
Docs-only; no gates.yaml edit, no floor, no test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Revision — verifier UNRESOLVED/MAJOR addressed (commit
|
Verification round (round-diff) — amendment-2 revision (
|
| stratum (≤2014) | obs | persons | positive-earnings obs | zero share |
|---|---|---|---|---|
| prime 25–44 | 162,955 | 22,358 | 137,732 | 0.155 |
| older 45–64 | 108,038 | 13,680 | 78,980 | 0.269 |
| 60–64 (the new bin) | 20,941 | 6,504 | 11,779 (~3,905 in the biennial 1998–2014 waves) | 0.438 |
| 55–59 (ref) | 25,058 | 8,020 | 17,647 | 0.296 |
Forward biennial donor pairs at 60–64 (≤2014): 5,352 — far above the k-NN k=25. So the 60–64 CellMarginal (plotting-position grid on ~3.9k positive obs) and donor pools are amply supported; the extension is genuinely ≤2014-derivable, not a thin bin. And the 60–64 zero share 0.438 vs 55–59's 0.296 confirms the retirement-transition zero-inflation earn_zero_rate.older scores is present and distinct at 60–64 — i.e. the old [25,59] clip really would have understated it by ~14pp (the exact signal-kill I flagged), and the fix captures it.
Per-finding resolution
MAJOR (fit range) — RESOLVED, code- and data-grounded. §2.7.6.5 now extends the forward participation gate, donor pools, memory ramp, and the NAWI-normalized marginal all to [25,64] pooled ≤2014; the age_bin grid goes to 8 bins {25–29 … 60–64} (certified was N_AGE_BINS = 7, candidate5b:155, which clip((age−25)//5, 0, 6) sends 60+ → 55–59 — this is exactly the distortion I flagged, so the certified code corroborates both the problem and the fix); clip moves to <25 → 25–29, >64 → 60–64. Both gated cohorts — prime 25–44, older 45–64 — are verified inside [25,64], cited to build_m6_holdout_floors.py:120. §2.7.6.2 also extends the marginal to the [25,64] grid and no longer claims the 55–59-clipped surface. The direction-of-fit principle is stated correctly: the law must cover the pre-registered truth-side surface, and narrowing it (re-scope/demote a gated cell) to fit the model's support would be candidate-informed surface design, prohibited in that direction; [25,59] is framed as a non-transferable hyperparameter. Resolution, not renaming.
MINOR 1 (round-trip) — RESOLVED. §2.7.6.4 now states interior-exact + corner/tie deterministic-but-inexact (quantile clamps [ymin,ymax], rank clamps [0.001,0.999], flat-yval tie → first wtil, candidate5b:250-259, tracked corner_bottom/top), with the consequence pinned: rank(ℓ) is a pure deterministic function of the carried level so chain integrity holds, the inexactness is only a bounded corner rank-perturbation, not a reproducibility break. The re_ranking_law JSON field is updated to match. Exactly the qualification I asked for.
MINOR 2 (NAWI publication lag) — RESOLVED, reasoning sound. §2.7.6.3 discloses that NAWI_2013/2014 are published ~2 years later, but the SSA AWI is a final, unrevised series (SSA does not restate prior AWI once set — a NAWI_y, y ≤ 2014, is a fixed function of ≤2014 wages), so only the publication date is post-T*, not the information — leakage-safe. The AWI-is-final claim is correct (AWI values are locked for benefit computation and not restated). The loose "same logic as the certified indexing convention" analogy is dropped.
MINOR 3 (mechanism vs object) — RESOLVED. §2.7.6.2 now reads "keeps the certified de-index MECHANISM — it re-fits a new age_bin NAWI-normalized object through the same CellMarginal machinery, not the certified (age_bin, period) object itself."
Read-only round-diff verification; no repo edits, no merge/ready. Verified against 722f59b, the certified candidate-5b machinery, and the live ≤2014 PSID panel.
…ash-merge Final re-finalization. §2.7.6 (PR #178, squash-merge 4c6a0f6, twice-verified -- VERIFIED comment 4960583620) COMPLETED the forward rank-to-level + re-ranking laws -- the forward earnings law the gate_m6 `covers` references. Per the block draft's own design_commit_note (finalize the pin to the squash-merge at the lock flip), the live design_commit is RE-FINALIZED from the interim #175 squash (ce9893b) to the #178 squash (4c6a0f6), so the lock references the COMPLETE law. Merges origin/master (#178, the §2.7.6 design-doc completion) and regenerates gates.gate_m6 from the (unchanged) draft VERBATIM with the four lock-time deltas (locked / status / +history / design_commit). The history design_commit_finalized record now carries from d6abb16 -> to 4c6a0f6 with refinalized_from ce9893b and the §2.7.6 reason; the content narrates the two-step finalization. Binding updates (tests/test_gate_m6_derivations.py): design_commit 4c6a0f6, the design_commit_finalized.refinalized_from + the §2.7.6 reason; the design-commit mutation now drifts the stale interim ce9893b. tests/test_gate_m6_floors.py asserts live design_commit == 4c6a0f6 (draft d6abb16 unchanged). runs/legacy_manifest_v1.json re-recorded via build_legacy_manifest.py --transition (contract_revision + the gates.yaml entry only). Tier counts unchanged (artifact 944, total 2,684). Sibling gates byte-identical to origin/master (4c6a0f6); gate_m6 the sole new key. Both design dependencies now satisfied: #175 (forward-law margin + re-pin) and #178 (§2.7.6 forward-law completion). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…177) * gate_m6 lock flip: temporal-holdout gate added locked (ratified 1818784) Adds gates.gate_m6 as a NEW LOCKED top-level gate (the gate_w1 / gate_m4 pattern -- added locked, NOT an in-place unlocked->locked flip). The block is docs/design/gate_m6_block_draft.yaml (the ratified v3 draft) carried VERBATIM with exactly three lock-time deltas: `locked: true`, `status: locked`, and a `history` ceremony entry. Every sibling gate (gate_1 / gate_2 [2a + 2b + 2c] / gate_3 / gate_m4 / gate_w1) is untouched; gate_m6 is the sole new key. Ceremony (gate_m6.history entry 2026-07-13-m6-gate-lock): the v1 OC-before-lock floor fired the PAUSE honestly (1 gated flow cell, combined p_gate 0.8449 -- a both-directions failure); the candidate-blind pinned-ladder redesign (coarsening + earnings decompounding, truth-side power arithmetic only) was adjudicated on the public record; v2 cleared 0.9067; the adversarial referee AMEND x4 (comment 4958425437) found the marital-ladder enumeration incomplete (the asymmetric age-2 rung where remarriage.18-64 clears at 0.403); fixes + the orchestrator's adjudicated v3 (comment 4958956779) landed 11 gated cells (5 flow + 6 earnings), p_seed 0.8934 / p_gate 0.9087; re-check PROCEED TO LOCK (comment 4959310892); ratified 2026-07-13 by merge of PR #172 (squash 1818784) under the maintainer's standing campaign directive of 2026-07-07. The headline not_certified declaration names mortality drift FIRST; the SSA/NCHS report-only mortality anchor is required before any M7-seeding claim; ZERO mortality / widowhood cells are gated on verified power grounds. ZERO threshold movement vs the frozen floor: the flip READS runs/m6_holdout_floors_v3.json (sha256 e931c886...) and rewrites nothing. Bindings go LOCKED-HOT: tests/test_gate_m6_derivations.py binds the live gates.yaml gate_m6 block to the v3 floor (11-cell registry equality, every tolerance recompute round(mean + k*sd, 3) capped, OC recompute 0.8934 / 0.9087 from the realized sigmas, not_certified ordering [mortality first], year pins / shock-window exclusions bound to design_pins, v1/v2/v3 lineage shas, SSA/NCHS anchor, presence / F6 / split-unit declarations) with >= 6 mutation checks (tolerance digit, cell add/drop, OC drift, not_certified reorder, year pin, history sha). Sibling / manifest hygiene: - tests/test_gate_m6_floors.py: the draft's untouched-guard inverts at the flip to assert gate_m6 is now LOCKED in gates.yaml (+ a verbatim-fidelity check: the live block equals the draft modulo the three deltas), mirroring the gate_w1 floors-test flip. - tests/test_gates_derivations.py + tests/test_gate_w1_derivations.py: the added-locked-gate master-compare tolerance extended with {"gate_m6"} (the a1-era generalization), with attribution. - tests/test_gate_w1_amendment2_proposal.py: the section-7 subset master-compare relaxed to master-side (tolerates the gate_m6 addition; every master gate stays present-and-equal in live except gate_w1). - runs/legacy_manifest_v1.json: recorded via build_legacy_manifest.py --transition (only contract_revision + the gates.yaml entry move; the frozen legacy gate-2 entries stay byte-identical). - tier recount (CI-equivalent full collection): artifact 928 -> 944 (+16, the new derivations file), total 2,668 -> 2,684 (tests/tier_counts.json + tests/README-tiers.md). Depends on the #175 block-draft amendment (forward-law first-certification in covers / not_certified + a design_commit re-pin), which lands before the flip-fidelity round; a follow-up commit will re-apply the updated draft verbatim + these same three lock-time deltas. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * gate_m6 lock flip: re-apply block from #175-amended draft + finalize design pin Follow-up to the #175 block-draft amendment (merged to master as ce9893b: the design 2.7 forward-earnings law -- first-certified by gate_m6, gate_1 does NOT cover it). Merges origin/master and RE-APPLIES gates.gate_m6 from the UPDATED docs/design/gate_m6_block_draft.yaml, still VERBATIM, with the lock-time deltas: - `locked: false -> true` - `status: draft_cleared_ready_for_lock_flip -> locked` - + the `2026-07-13-m6-gate-lock` history entry - `design_commit` FINALIZED per the draft's own design_commit_note from PR #175's design commit `d6abb16b` to the #175 squash-merge `ce9893b` (`design_pr: "175"` carried verbatim). The amendment carries into the live block verbatim: the extended `covers` (forward-law first-certification framing) and the new `not_certified` margin `forward_earnings_law_not_gate1_certified` (9 -> 10 margins). The history entry records the finalization (`design_commit_finalized`: from / to / rule / note) and narrates it in `content`. Binding updates (`tests/test_gate_m6_derivations.py`): `design_pr "175"`, `design_commit ce9893b`, the 10-margin `not_certified` count + the forward-law margin, and the history finalization record are now bound; +2 mutations (un-finalized design_commit; dropped forward-law margin) -> 12 mutation checks. `tests/test_gate_m6_floors.py` verbatim-fidelity check extended to tolerate the design_commit delta (live == draft modulo locked / status / history / design_commit; design_pr stays "175"). `runs/legacy_manifest_v1.json` re-recorded via `build_legacy_manifest.py --transition` (contract_revision + the gates.yaml entry only). Tier counts unchanged (artifact 944, total 2,684 -- CI-equivalent recount confirms no drift from the merge). Sibling gates byte-identical to origin/master (ce9893b); gate_m6 the sole new key. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * gate_m6 lock flip: re-finalize design_commit to the §2.7.6 (#178) squash-merge Final re-finalization. §2.7.6 (PR #178, squash-merge 4c6a0f6, twice-verified -- VERIFIED comment 4960583620) COMPLETED the forward rank-to-level + re-ranking laws -- the forward earnings law the gate_m6 `covers` references. Per the block draft's own design_commit_note (finalize the pin to the squash-merge at the lock flip), the live design_commit is RE-FINALIZED from the interim #175 squash (ce9893b) to the #178 squash (4c6a0f6), so the lock references the COMPLETE law. Merges origin/master (#178, the §2.7.6 design-doc completion) and regenerates gates.gate_m6 from the (unchanged) draft VERBATIM with the four lock-time deltas (locked / status / +history / design_commit). The history design_commit_finalized record now carries from d6abb16 -> to 4c6a0f6 with refinalized_from ce9893b and the §2.7.6 reason; the content narrates the two-step finalization. Binding updates (tests/test_gate_m6_derivations.py): design_commit 4c6a0f6, the design_commit_finalized.refinalized_from + the §2.7.6 reason; the design-commit mutation now drifts the stale interim ce9893b. tests/test_gate_m6_floors.py asserts live design_commit == 4c6a0f6 (draft d6abb16 unchanged). runs/legacy_manifest_v1.json re-recorded via build_legacy_manifest.py --transition (contract_revision + the gates.yaml entry only). Tier counts unchanged (artifact 944, total 2,684). Sibling gates byte-identical to origin/master (4c6a0f6); gate_m6 the sole new key. Both design dependencies now satisfied: #175 (forward-law margin + re-pin) and #178 (§2.7.6 forward-law completion). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
M6 design amendment 2 — forward rank-to-level + re-ranking laws (§2.7.6)
Adds §2.7.6 to
docs/design/m6_projection_engine.md(design doc revision 3),closing the engine build's round-2 blocker. Docs-only, DRAFT; no
gates.yamledit,no floor, no test.
Why (Sol's round-2 blocker)
~/PolicyEngine/sol-worktrees/m6-forward-REPORT.md: §2.7 pinned the forwardconditional-rank draw and the real per-year seam, but never specified the
leakage-safe map from a drawn rank to a positive earnings level for 2016/2018,
nor its inverse (recovering the drawn-2016 rank at the 2018 step). The certified
de-index (
run_gate1_candidate7.py:648-654) maps the drawn rank through a(age_bin, period)CellMarginalquantile — and a≤2014fit has no(age_bin, 2016)cell, which §2.7.1 forbids inventing. Sol correctly stoppedrather than choose one of several materially different laws.
What §2.7.6 pins (zero design choices left)
≤2014age_binCellMarginal(the certifiedfit_cell_marginalsmachinery verbatim,only re-keyed
(age_bin,period)→age_binon normalized levels); de-index a drawnrank through its
Qhat_posquantile, then re-index to the target year by a≤2014-projected wage index. Rejected: (a) donor-level bootstrap (thecertified de-index is a marginal quantile, not a donor-level carry —
_knn_drawreturns the donor's rank; an R2 departure with no clean inverse);(b1) carrying the 2014 period marginal (§2.7.1-prohibited, biases
earn_p10low).
I_proj+ leakage prohibition.I_proj(2016/2018)= OLSln(NAWI)~yearover the trailing decade[2005,2014], extrapolated. Usingrealized post-
T*NAWI on the scored path is PROHIBITED — it would leakrealized wage growth into the gated level cells. Concretely, the frame's realized
nawicolumn (the aging step) is fenced off the scored de-index and admissibleonly for the non-scored seam /
taxable_maxand the report-only alignmentpath (decision 9, §4.8; the M2
calibration_disclosurelesson — the gated pathis the un-aligned projection).
rhat, the exact inverse ofthe rank-to-level map, so rank→level→rank round-trips). Rejected: within-frame
percentile (references the synthetic projected cohort, not the PSID fit
population — an R2 violation that breaks the round-trip). R2: the certified
chain's ranks are
CellMarginalCDF positions, so the deployed re-rank mustreference the fit-population marginal.
generated-lag columns (
gen_earn_w2/w4); theseed_from_generator → SeedSequence([seed, code])substream bridge (the existingm6-enginehelper +SUBSTREAM_CODES); the concrete frame schema (u_w,realized_earn_2014/2012,the generated-lag columns; materialized by the
≤2014refit); and age-binclipping to the fitted
[25,59]range.Lock dependency
§2.7.6.6 states plainly that this closes Sol's round-2 blocker and that the
gate_m6lock flip'sdesign_commitmust finalize to this amendment's mergecommit (not the round-2
d6abb16) — thegate_m6lock is holding for it.Not in scope
No
gates.yamledit, no floor, no test, no code. Sol implements the forwardgenerator against §2.7 + §2.7.6 next.
🤖 Generated with Claude Code